/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.javafxports.jfxmobile.plugin.android.task;

import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.gradle.internal.dsl.PackagingOptions;
import com.android.sdklib.build.ApkCreationException;
import com.android.sdklib.build.DuplicateFileException;
import com.android.sdklib.build.IArchiveBuilder;
import com.android.sdklib.build.SealedApkException;
import com.android.sdklib.internal.build.DebugKeyProvider;
import com.android.sdklib.internal.build.DebugKeyProvider.IKeyGenOutput;
import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException;
import com.android.sdklib.internal.build.SignedJarBuilder.IZipEntryFilter;
import com.google.common.collect.Sets;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * Class making the final apk packaging.
 * The inputs are:
 * - packaged resources (output of aapt)
 * - code file (output of dx)
 * - Java resources coming from the project, its libraries, and its jar files
 * - Native libraries from the project or its library.
 *
 */
public final class ApkBuilder implements IArchiveBuilder {

    private static final Pattern PATTERN_NATIVELIB_EXT = Pattern.compile("^.+\\.so$",
            Pattern.CASE_INSENSITIVE);
    private static final Pattern PATTERN_BITCODELIB_EXT = Pattern.compile("^.+\\.bc$",
            Pattern.CASE_INSENSITIVE);

    /**
     * A No-op zip filter. It's used to detect conflicts.
     *
     */
    private final class NullZipFilter implements IZipEntryFilter {
        private File mInputFile;

        void reset(File inputFile) {
            mInputFile = inputFile;
        }

        @Override
        public boolean checkEntry(String archivePath) throws ZipAbortException {
            verbosePrintln("=> %s", archivePath);

            File duplicate = checkFileForDuplicate(archivePath);
            if (duplicate != null) {
                throw new DuplicateFileException(archivePath, duplicate, mInputFile);
            } else {
                mAddedFiles.put(archivePath, mInputFile);
            }

            return true;
        }
    }

    /**
     * Custom {@link IZipEntryFilter} to filter out everything that is not a standard java
     * resources, and also record whether the zip file contains native libraries.
     * <p>Used in {@link SignedJarBuilder#writeZip(java.io.InputStream, IZipEntryFilter)} when
     * we only want the java resources from external jars.
     */
    private final class JavaAndNativeResourceFilter implements IZipEntryFilter {
        private final List<String> mNativeLibs = new ArrayList<String>();
        private boolean mNativeLibsConflict = false;
        private File mInputFile;

        private Set<String> mUsedPickFirsts = null;

        @Nullable
        private final PackagingOptions mPackagingOptions;

        @NonNull
        private final Set<String> mExcludes;
        @NonNull
        private final Set<String> mPickFirsts;

        private JavaAndNativeResourceFilter(@Nullable PackagingOptions packagingOptions) {
            mPackagingOptions = packagingOptions;

            mExcludes = mPackagingOptions != null ? mPackagingOptions.getExcludes() :
                    Collections.<String>emptySet();
            mPickFirsts = mPackagingOptions != null ? mPackagingOptions.getPickFirsts() :
                    Collections.<String>emptySet();
        }

        @Override
        public boolean checkEntry(String archivePath) throws ZipAbortException {
            // split the path into segments.
            String[] segments = archivePath.split("/");

            // empty path? skip to next entry.
            if (segments.length == 0) {
                return false;
            }

            //noinspection VariableNotUsedInsideIf
            if (mPackagingOptions != null) {
                if (mExcludes.contains(archivePath)) {
                    return false;
                }

                if (mPickFirsts.contains(archivePath)) {
                    if (mUsedPickFirsts == null) {
                        mUsedPickFirsts = Sets.newHashSetWithExpectedSize(mPickFirsts.size());
                    }

                    if (mUsedPickFirsts.contains(archivePath)) {
                        return false;
                    } else {
                        mUsedPickFirsts.add(archivePath);
                    }
                }
            }

            // Check each folders to make sure they should be included.
            // Folders like CVS, .svn, etc.. should already have been excluded from the
            // jar file, but we need to exclude some other folder (like /META-INF) so
            // we check anyway.
            for (int i = 0 ; i < segments.length - 1; i++) {
                if (!checkFolderForPackaging(segments[i])) {
                    return false;
                }
            }

            // get the file name from the path
            String fileName = segments[segments.length-1];

            boolean check = checkFileForPackaging(fileName);

            // only do additional checks if the file passes the default checks.
            if (check) {
                verbosePrintln("=> %s", archivePath);

                File duplicate = checkFileForDuplicate(archivePath);
                if (duplicate != null) {
                    throw new DuplicateFileException(archivePath, duplicate, mInputFile);
                } else {
                    mAddedFiles.put(archivePath, mInputFile);
                }

                if (archivePath.endsWith(".so") || archivePath.endsWith(".bc")) {
                    mNativeLibs.add(archivePath);

                    // only .so located in lib/ will interfere with the installation
                    if (archivePath.startsWith(SdkConstants.FD_APK_NATIVE_LIBS + "/")) {
                        mNativeLibsConflict = true;
                    }
                } else if (archivePath.endsWith(".jnilib")) {
                    mNativeLibs.add(archivePath);
                }
            }

            return check;
        }

        List<String> getNativeLibs() {
            return mNativeLibs;
        }

        boolean getNativeLibsConflict() {
            return mNativeLibsConflict;
        }

        void reset(File inputFile) {
            mInputFile = inputFile;
            mNativeLibs.clear();
            mNativeLibsConflict = false;
        }
    }

    private File mApkFile;
    private File mResFile;
    private File mDexFile;
    private PrintStream mVerboseStream;
    private SignedJarBuilder mBuilder;
    private boolean mDebugMode = false;
    private boolean mIsSealed = false;

    private final NullZipFilter mNullFilter = new NullZipFilter();
    private final JavaAndNativeResourceFilter mFilter;
    private final HashMap<String, File> mAddedFiles = new HashMap<String, File>();

    /**
     * Status for the addition of a jar file resources into the APK.
     * This indicates possible issues with native library inside the jar file.
     */
    public interface JarStatus {
        /**
         * Returns the list of native libraries found in the jar file.
         */
        List<String> getNativeLibs();

        /**
         * Returns whether some of those libraries were located in the location that Android
         * expects its native libraries.
         */
        boolean hasNativeLibsConflicts();

    }

    /** Internal implementation of {@link JarStatus}. */
    private static final class JarStatusImpl implements JarStatus {
        public final List<String> mLibs;
        public final boolean mNativeLibsConflict;

        private JarStatusImpl(List<String> libs, boolean nativeLibsConflict) {
            mLibs = libs;
            mNativeLibsConflict = nativeLibsConflict;
        }

        @Override
        public List<String> getNativeLibs() {
            return mLibs;
        }

        @Override
        public boolean hasNativeLibsConflicts() {
            return mNativeLibsConflict;
        }
    }

    /**
     * Signing information.
     *
     * Both the {@link PrivateKey} and the {@link X509Certificate} are guaranteed to be non-null.
     *
     */
    public static final class SigningInfo {
        public final PrivateKey key;
        public final X509Certificate certificate;

        private SigningInfo(PrivateKey key, X509Certificate certificate) {
            if (key == null || certificate == null) {
                throw new IllegalArgumentException("key and certificate cannot be null");
            }
            this.key = key;
            this.certificate = certificate;
        }
    }

    /**
     * Returns the key and certificate from a given debug store.
     *
     * It is expected that the store password is 'android' and the key alias and password are
     * 'androiddebugkey' and 'android' respectively.
     *
     * @param storeOsPath the OS path to the debug store.
     * @param verboseStream an option {@link PrintStream} to display verbose information
     * @return they key and certificate in a {@link SigningInfo} object or null.
     * @throws ApkCreationException
     */
    public static SigningInfo getDebugKey(String storeOsPath, final PrintStream verboseStream)
            throws ApkCreationException {
        try {
            if (storeOsPath != null) {
                File storeFile = new File(storeOsPath);
                try {
                    checkInputFile(storeFile);
                } catch (FileNotFoundException e) {
                    // ignore these since the debug store can be created on the fly anyway.
                }

                // get the debug key
                if (verboseStream != null) {
                    verboseStream.println(String.format("Using keystore: %s", storeOsPath));
                }

                IKeyGenOutput keygenOutput = null;
                if (verboseStream != null) {
                    keygenOutput = new IKeyGenOutput() {
                        @Override
                        public void out(String message) {
                            verboseStream.println(message);
                        }

                        @Override
                        public void err(String message) {
                            verboseStream.println(message);
                        }
                    };
                }

                DebugKeyProvider keyProvider = new DebugKeyProvider(
                        storeOsPath, null /*store type*/, keygenOutput);

                PrivateKey key = keyProvider.getDebugKey();
                X509Certificate certificate = (X509Certificate)keyProvider.getCertificate();

                if (key == null) {
                    throw new ApkCreationException("Unable to get debug signature key");
                }

                // compare the certificate expiration date
                if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) {
                    // TODO, regenerate a new one.
                    throw new ApkCreationException("Debug Certificate expired on " +
                            DateFormat.getInstance().format(certificate.getNotAfter()));
                }

                return new SigningInfo(key, certificate);
            } else {
                return null;
            }
        } catch (KeytoolException e) {
            if (e.getJavaHome() == null) {
                throw new ApkCreationException(e.getMessage() +
                        "\nJAVA_HOME seems undefined, setting it will help locating keytool automatically\n" +
                        "You can also manually execute the following command\n:" +
                        e.getCommandLine(), e);
            } else {
                throw new ApkCreationException(e.getMessage() +
                        "\nJAVA_HOME is set to: " + e.getJavaHome() +
                        "\nUpdate it if necessary, or manually execute the following command:\n" +
                        e.getCommandLine(), e);
            }
        } catch (ApkCreationException e) {
            throw e;
        } catch (Exception e) {
            throw new ApkCreationException(e);
        }
    }

    /**
     * Creates a new instance.
     *
     * This creates a new builder that will create the specified output file, using the two
     * mandatory given input files.
     *
     * An optional debug keystore can be provided. If set, it is expected that the store password
     * is 'android' and the key alias and password are 'androiddebugkey' and 'android'.
     *
     * An optional {@link PrintStream} can also be provided for verbose output. If null, there will
     * be no output.
     *
     * @param apkOsPath the OS path of the file to create.
     * @param resOsPath the OS path of the packaged resource file.
     * @param dexOsPath the OS path of the dex file. This can be null for apk with no code.
     * @param verboseStream the stream to which verbose output should go. If null, verbose mode
     *                      is not enabled.
     * @throws ApkCreationException
     */
    public ApkBuilder(String apkOsPath, String resOsPath, String dexOsPath, String storeOsPath,
            PackagingOptions packagingOptions, PrintStream verboseStream) throws ApkCreationException {
        this(new File(apkOsPath),
             new File(resOsPath),
             dexOsPath != null ? new File(dexOsPath) : null,
             storeOsPath,
             packagingOptions,
             verboseStream);
    }

    /**
     * Creates a new instance.
     *
     * This creates a new builder that will create the specified output file, using the two
     * mandatory given input files.
     *
     * Optional {@link PrivateKey} and {@link X509Certificate} can be provided to sign the APK.
     *
     * An optional {@link PrintStream} can also be provided for verbose output. If null, there will
     * be no output.
     *
     * @param apkOsPath the OS path of the file to create.
     * @param resOsPath the OS path of the packaged resource file.
     * @param dexOsPath the OS path of the dex file. This can be null for apk with no code.
     * @param key the private key used to sign the package. Can be null.
     * @param certificate the certificate used to sign the package. Can be null.
     * @param verboseStream the stream to which verbose output should go. If null, verbose mode
     *                      is not enabled.
     * @throws ApkCreationException
     */
    public ApkBuilder(String apkOsPath, String resOsPath, String dexOsPath, PrivateKey key,
                      X509Certificate certificate, PackagingOptions packagingOptions,
                      PrintStream verboseStream) throws ApkCreationException {
        this(new File(apkOsPath),
             new File(resOsPath),
             dexOsPath != null ? new File(dexOsPath) : null,
             key, certificate, packagingOptions,
             verboseStream);
    }

    /**
     * Creates a new instance.
     *
     * This creates a new builder that will create the specified output file, using the two
     * mandatory given input files.
     *
     * An optional debug keystore can be provided. If set, it is expected that the store password
     * is 'android' and the key alias and password are 'androiddebugkey' and 'android'.
     *
     * An optional {@link PrintStream} can also be provided for verbose output. If null, there will
     * be no output.
     *
     * @param apkFile the file to create
     * @param resFile the file representing the packaged resource file.
     * @param dexFile the file representing the dex file. This can be null for apk with no code.
     * @param debugStoreOsPath the OS path to the debug keystore, if needed or null.
     * @param verboseStream the stream to which verbose output should go. If null, verbose mode
     *                      is not enabled.
     * @throws ApkCreationException
     */
    public ApkBuilder(File apkFile, File resFile, File dexFile, String debugStoreOsPath,
            PackagingOptions packagingOptions, final PrintStream verboseStream) throws ApkCreationException {
        mFilter = new JavaAndNativeResourceFilter(packagingOptions);

        SigningInfo info = getDebugKey(debugStoreOsPath, verboseStream);
        if (info != null) {
            init(apkFile, resFile, dexFile, info.key, info.certificate, verboseStream);
        } else {
            init(apkFile, resFile, dexFile, null /*key*/, null/*certificate*/, verboseStream);
        }
    }

    /**
     * Creates a new instance.
     *
     * This creates a new builder that will create the specified output file, using the two
     * mandatory given input files.
     *
     * Optional {@link PrivateKey} and {@link X509Certificate} can be provided to sign the APK.
     *
     * An optional {@link PrintStream} can also be provided for verbose output. If null, there will
     * be no output.
     *
     * @param apkFile the file to create
     * @param resFile the file representing the packaged resource file.
     * @param dexFile the file representing the dex file. This can be null for apk with no code.
     * @param key the private key used to sign the package. Can be null.
     * @param certificate the certificate used to sign the package. Can be null.
     * @param packagingOptions
     * @param verboseStream the stream to which verbose output should go. If null, verbose mode
     *                      is not enabled.
     * @throws ApkCreationException
     */
    public ApkBuilder(File apkFile, File resFile, File dexFile, PrivateKey key,
            X509Certificate certificate, PackagingOptions packagingOptions, PrintStream verboseStream) throws ApkCreationException {
        mFilter = new JavaAndNativeResourceFilter(packagingOptions);

        init(apkFile, resFile, dexFile, key, certificate, verboseStream);
    }


    /**
     * Constructor init method.
     *
     * @see #ApkBuilder(File, File, File, String, PackagingOptions, PrintStream)
     * @see #ApkBuilder(String, String, String, String, PackagingOptions, PrintStream)
     * @see #ApkBuilder(File, File, File, PrivateKey, X509Certificate, PackagingOptions, PrintStream)
     */
    private void init(File apkFile, File resFile, File dexFile, PrivateKey key,
            X509Certificate certificate, PrintStream verboseStream) throws ApkCreationException {

        try {
            checkOutputFile(mApkFile = apkFile);
            checkInputFile(mResFile = resFile);
            if (dexFile != null) {
                checkInputFile(mDexFile = dexFile);
            } else {
                mDexFile = null;
            }
            mVerboseStream = verboseStream;

            mBuilder = new SignedJarBuilder(
                    new FileOutputStream(mApkFile, false /* append */), key,
                    certificate);

            verbosePrintln("Packaging %s", mApkFile.getName());

            // add the resources
            addZipFile(mResFile);

            // add the class dex file at the root of the apk
            if (mDexFile != null) {
                addFile(mDexFile, SdkConstants.FN_APK_CLASSES_DEX);
            }

        } catch (ApkCreationException e) {
            if (mBuilder != null) {
                mBuilder.cleanUp();
            }
            throw e;
        } catch (Exception e) {
            if (mBuilder != null) {
                mBuilder.cleanUp();
            }
            throw new ApkCreationException(e);
        }
    }

    /**
     * Sets the debug mode. In debug mode, when native libraries are present, the packaging
     * will also include one or more copies of gdbserver in the final APK file.
     *
     * These are used for debugging native code, to ensure that gdbserver is accessible to the
     * application.
     *
     * There will be one version of gdbserver for each ABI supported by the application.
     *
     * the gbdserver files are placed in the libs/abi/ folders automatically by the NDK.
     *
     * @param debugMode the debug mode flag.
     */
    public void setDebugMode(boolean debugMode) {
        mDebugMode = debugMode;
    }

    /**
     * Adds a file to the APK at a given path
     * @param file the file to add
     * @param archivePath the path of the file inside the APK archive.
     * @throws ApkCreationException if an error occurred
     * @throws SealedApkException if the APK is already sealed.
     * @throws DuplicateFileException if a file conflicts with another already added to the APK
     *                                   at the same location inside the APK archive.
     */
    @Override
    public void addFile(File file, String archivePath) throws ApkCreationException,
            SealedApkException, DuplicateFileException {
        if (mIsSealed) {
            throw new SealedApkException("APK is already sealed");
        }

        try {
            doAddFile(file, archivePath);
        } catch (DuplicateFileException e) {
            mBuilder.cleanUp();
            throw e;
        } catch (Exception e) {
            mBuilder.cleanUp();
            throw new ApkCreationException(e, "Failed to add %s", file);
        }
    }

    /**
     * Adds the content from a zip file.
     * All file keep the same path inside the archive.
     * @param zipFile the zip File.
     * @throws ApkCreationException if an error occurred
     * @throws SealedApkException if the APK is already sealed.
     * @throws DuplicateFileException if a file conflicts with another already added to the APK
     *                                   at the same location inside the APK archive.
     */
    public void addZipFile(File zipFile) throws ApkCreationException, SealedApkException,
            DuplicateFileException {
        if (mIsSealed) {
            throw new SealedApkException("APK is already sealed");
        }

        try {
            verbosePrintln("%s:", zipFile);

            // reset the filter with this input.
            mNullFilter.reset(zipFile);

            // ask the builder to add the content of the file.
            FileInputStream fis = new FileInputStream(zipFile);
            mBuilder.writeZip(fis, mNullFilter);
            fis.close();
        } catch (DuplicateFileException e) {
            mBuilder.cleanUp();
            throw e;
        } catch (Exception e) {
            mBuilder.cleanUp();
            throw new ApkCreationException(e, "Failed to add %s", zipFile);
        }
    }

    /**
     * Adds the resources from a jar file.
     * @param jarFile the jar File.
     * @return a {@link JarStatus} object indicating if native libraries where found in
     *         the jar file.
     * @throws ApkCreationException if an error occurred
     * @throws SealedApkException if the APK is already sealed.
     * @throws DuplicateFileException if a file conflicts with another already added to the APK
     *                                   at the same location inside the APK archive.
     */
    public JarStatus addResourcesFromJar(File jarFile) throws ApkCreationException,
            SealedApkException, DuplicateFileException {
        if (mIsSealed) {
            throw new SealedApkException("APK is already sealed");
        }

        try {
            verbosePrintln("%s:", jarFile);

            // reset the filter with this input.
            mFilter.reset(jarFile);

            // ask the builder to add the content of the file, filtered to only let through
            // the java resources.
            FileInputStream fis = new FileInputStream(jarFile);
            mBuilder.writeZip(fis, mFilter);
            fis.close();

            // check if native libraries were found in the external library. This should
            // constitutes an error or warning depending on if they are in lib/
            return new JarStatusImpl(mFilter.getNativeLibs(), mFilter.getNativeLibsConflict());
        } catch (DuplicateFileException e) {
            mBuilder.cleanUp();
            throw e;
        } catch (Exception e) {
            mBuilder.cleanUp();
            throw new ApkCreationException(e, "Failed to add %s", jarFile);
        }
    }

    /**
     * Adds the resources from a source folder.
     * @param sourceFolder the source folder.
     * @throws ApkCreationException if an error occurred
     * @throws SealedApkException if the APK is already sealed.
     * @throws DuplicateFileException if a file conflicts with another already added to the APK
     *                                   at the same location inside the APK archive.
     */
    public void addSourceFolder(File sourceFolder) throws ApkCreationException, SealedApkException,
            DuplicateFileException {
        if (mIsSealed) {
            throw new SealedApkException("APK is already sealed");
        }

        addSourceFolder(this, sourceFolder);
    }

    /**
     * Adds the resources from a source folder to a given {@link IArchiveBuilder}
     * @param sourceFolder the source folder.
     * @throws ApkCreationException if an error occurred
     * @throws DuplicateFileException if a file conflicts with another already added to the APK
     *                                   at the same location inside the APK archive.
     */
    public static void addSourceFolder(IArchiveBuilder builder, File sourceFolder)
            throws ApkCreationException, DuplicateFileException {
        if (sourceFolder.isDirectory()) {
            try {
                // file is a directory, process its content.
                File[] files = sourceFolder.listFiles();
                if (files != null) {
                    for (File file : files) {
                        processFileForResource(builder, file, null);
                    }
                }
            } catch (DuplicateFileException e) {
                throw e;
            } catch (Exception e) {
                throw new ApkCreationException(e, "Failed to add %s", sourceFolder);
            }
        } else {
            // not a directory? check if it's a file or doesn't exist
            if (sourceFolder.exists()) {
                throw new ApkCreationException("%s is not a folder", sourceFolder);
            } else {
                throw new ApkCreationException("%s does not exist", sourceFolder);
            }
        }
    }

    /**
     * Adds the native libraries from the top native folder.
     * The content of this folder must be the various ABI folders.
     *
     * This may or may not copy gdbserver into the apk based on whether the debug mode is set.
     *
     * @param nativeFolder the native folder.
     *
     * @throws ApkCreationException if an error occurred
     * @throws SealedApkException if the APK is already sealed.
     * @throws DuplicateFileException if a file conflicts with another already added to the APK
     *                                   at the same location inside the APK archive.
     *
     * @see #setDebugMode(boolean)
     */
    public void addNativeLibraries(File nativeFolder)
            throws ApkCreationException, SealedApkException, DuplicateFileException {
        if (mIsSealed) {
            throw new SealedApkException("APK is already sealed");
        }

        if (!nativeFolder.isDirectory()) {
            // not a directory? check if it's a file or doesn't exist
            if (nativeFolder.exists()) {
                throw new ApkCreationException("%s is not a folder", nativeFolder);
            } else {
                throw new ApkCreationException("%s does not exist", nativeFolder);
            }
        }

        File[] abiList = nativeFolder.listFiles();

        verbosePrintln("Native folder: %s", nativeFolder);

        if (abiList != null) {
            for (File abi : abiList) {
                if (abi.isDirectory()) { // ignore files

                    File[] libs = abi.listFiles();
                    if (libs != null) {
                        for (File lib : libs) {
                            // only consider files that are .so or, if in debug mode, that
                            // are gdbserver executables
                            if (lib.isFile() &&
                                    (PATTERN_NATIVELIB_EXT.matcher(lib.getName()).matches() ||
                                    PATTERN_BITCODELIB_EXT.matcher(lib.getName()).matches() ||
                                            (mDebugMode &&
                                                    SdkConstants.FN_GDBSERVER.equals(
                                                            lib.getName())))) {
                                String path =
                                    SdkConstants.FD_APK_NATIVE_LIBS + "/" +
                                    abi.getName() + "/" + lib.getName();

                                try {
                                    doAddFile(lib, path);
                                } catch (IOException e) {
                                    mBuilder.cleanUp();
                                    throw new ApkCreationException(e, "Failed to add %s", lib);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    public void addNativeLibraries(List<FileEntry> entries) throws SealedApkException,
            DuplicateFileException, ApkCreationException {
        if (mIsSealed) {
            throw new SealedApkException("APK is already sealed");
        }

        for (FileEntry entry : entries) {
            try {
                doAddFile(entry.mFile, entry.mPath);
            } catch (IOException e) {
                mBuilder.cleanUp();
                throw new ApkCreationException(e, "Failed to add %s", entry.mFile);
            }
        }
    }

    public static final class FileEntry {
        public final File mFile;
        public final String mPath;

        FileEntry(File file, String path) {
            mFile = file;
            mPath = path;
        }
    }

    public static List<FileEntry> getNativeFiles(File nativeFolder, boolean debugMode)
            throws ApkCreationException  {

        if (!nativeFolder.isDirectory()) {
            // not a directory? check if it's a file or doesn't exist
            if (nativeFolder.exists()) {
                throw new ApkCreationException("%s is not a folder", nativeFolder);
            } else {
                throw new ApkCreationException("%s does not exist", nativeFolder);
            }
        }

        List<FileEntry> files = new ArrayList<FileEntry>();

        File[] abiList = nativeFolder.listFiles();

        if (abiList != null) {
            for (File abi : abiList) {
                if (abi.isDirectory()) { // ignore files

                    File[] libs = abi.listFiles();
                    if (libs != null) {
                        for (File lib : libs) {
                            // only consider files that are .so or, if in debug mode, that
                            // are gdbserver executables
                            if (lib.isFile() &&
                                    (PATTERN_NATIVELIB_EXT.matcher(lib.getName()).matches() ||
                                    PATTERN_BITCODELIB_EXT.matcher(lib.getName()).matches() ||
                                            (debugMode &&
                                                    SdkConstants.FN_GDBSERVER.equals(
                                                            lib.getName())))) {
                                String path =
                                    SdkConstants.FD_APK_NATIVE_LIBS + "/" +
                                    abi.getName() + "/" + lib.getName();

                                files.add(new FileEntry(lib, path));
                            }
                        }
                    }
                }
            }
        }

        return files;
    }



    /**
     * Seals the APK, and signs it if necessary.
     * @throws ApkCreationException
     * @throws ApkCreationException if an error occurred
     * @throws SealedApkException if the APK is already sealed.
     */
    public void sealApk() throws ApkCreationException, SealedApkException {
        if (mIsSealed) {
            throw new SealedApkException("APK is already sealed");
        }

        // close and sign the application package.
        try {
            mBuilder.close();
            mIsSealed = true;
        } catch (Exception e) {
            throw new ApkCreationException(e, "Failed to seal APK");
        } finally {
            mBuilder.cleanUp();
        }
    }

    /**
     * Output a given message if the verbose mode is enabled.
     * @param format the format string for {@link String#format(String, Object...)}
     * @param args the string arguments
     */
    private void verbosePrintln(String format, Object... args) {
        if (mVerboseStream != null) {
            mVerboseStream.println(String.format(format, args));
        }
    }

    private void doAddFile(File file, String archivePath) throws DuplicateFileException,
            IOException {
        verbosePrintln("%1$s => %2$s", file, archivePath);

        File duplicate = checkFileForDuplicate(archivePath);
        if (duplicate != null) {
            throw new DuplicateFileException(archivePath, duplicate, file);
        }

        mAddedFiles.put(archivePath, file);
        mBuilder.writeFile(file, archivePath);
    }

    /**
     * Processes a {@link File} that could be an APK {@link File}, or a folder containing
     * java resources.
     *
     * @param file the {@link File} to process.
     * @param path the relative path of this file to the source folder.
     *          Can be <code>null</code> to identify a root file.
     * @throws IOException
     * @throws DuplicateFileException if a file conflicts with another already added
     *          to the APK at the same location inside the APK archive.
     * @throws SealedApkException if the APK is already sealed.
     * @throws ApkCreationException if an error occurred
     */
    private static void processFileForResource(IArchiveBuilder builder, File file, String path)
            throws IOException, DuplicateFileException, ApkCreationException, SealedApkException {
        if (file.isDirectory()) {
            // a directory? we check it
            if (checkFolderForPackaging(file.getName())) {
                // if it's valid, we append its name to the current path.
                if (path == null) {
                    path = file.getName();
                } else {
                    path = path + "/" + file.getName();
                }

                // and process its content.
                File[] files = file.listFiles();
                if (files != null) {
                    for (File contentFile : files) {
                        processFileForResource(builder, contentFile, path);
                    }
                }
            }
        } else {
            // a file? we check it to make sure it should be added
            if (checkFileForPackaging(file.getName())) {
                // we append its name to the current path
                if (path == null) {
                    path = file.getName();
                } else {
                    path = path + "/" + file.getName();
                }

                // and add it to the apk
                builder.addFile(file, path);
            }
        }
    }

    /**
     * Checks if the given path in the APK archive has not already been used and if it has been,
     * then returns a {@link File} object for the source of the duplicate
     * @param archivePath the archive path to test.
     * @return A File object of either a file at the same location or an archive that contains a
     * file that was put at the same location.
     */
    private File checkFileForDuplicate(String archivePath) {
        return mAddedFiles.get(archivePath);
    }

    /**
     * Checks an output {@link File} object.
     * This checks the following:
     * - the file is not an existing directory.
     * - if the file exists, that it can be modified.
     * - if it doesn't exists, that a new file can be created.
     * @param file the File to check
     * @throws ApkCreationException If the check fails
     */
    private void checkOutputFile(File file) throws ApkCreationException {
        if (file.isDirectory()) {
            throw new ApkCreationException("%s is a directory!", file);
        }

        if (file.exists()) { // will be a file in this case.
            if (!file.canWrite()) {
                throw new ApkCreationException("Cannot write %s", file);
            }
        } else {
            try {
                if (!file.createNewFile()) {
                    throw new ApkCreationException("Failed to create %s", file);
                }
            } catch (IOException e) {
                throw new ApkCreationException(
                        "Failed to create '%1$ss': %2$s", file, e.getMessage());
            }
        }
    }

    /**
     * Checks an input {@link File} object.
     * This checks the following:
     * - the file is not an existing directory.
     * - that the file exists (if <var>throwIfDoesntExist</var> is <code>false</code>) and can
     *    be read.
     * @param file the File to check
     * @throws FileNotFoundException if the file is not here.
     * @throws ApkCreationException If the file is a folder or a file that cannot be read.
     */
    private static void checkInputFile(File file) throws FileNotFoundException, ApkCreationException {
        if (file.isDirectory()) {
            throw new ApkCreationException("%s is a directory!", file);
        }

        if (file.exists()) {
            if (!file.canRead()) {
                throw new ApkCreationException("Cannot read %s", file);
            }
        } else {
            throw new FileNotFoundException(String.format("%s does not exist", file));
        }
    }

    public static String getDebugKeystore() throws ApkCreationException {
        try {
            return DebugKeyProvider.getDefaultKeyStoreOsPath();
        } catch (Exception e) {
            throw new ApkCreationException(e, e.getMessage());
        }
    }

    /**
     * Checks whether a folder and its content is valid for packaging into the .apk as
     * standard Java resource.
     * @param folderName the name of the folder.
     */
    public static boolean checkFolderForPackaging(String folderName) {
        return !folderName.equalsIgnoreCase("CVS") &&
                !folderName.equalsIgnoreCase(".svn") &&
                !folderName.equalsIgnoreCase("SCCS") &&
                !folderName.startsWith("_");
    }

    /**
     * Checks a file to make sure it should be packaged as standard resources.
     * @param fileName the name of the file (including extension)
     * @return true if the file should be packaged as standard java resources.
     */
    public static boolean checkFileForPackaging(String fileName) {
        String[] fileSegments = fileName.split("\\.");
        String fileExt = "";
        if (fileSegments.length > 1) {
            fileExt = fileSegments[fileSegments.length-1];
        }

        return checkFileForPackaging(fileName, fileExt);
    }

    /**
     * Checks a file to make sure it should be packaged as standard resources.
     * @param fileName the name of the file (including extension)
     * @param extension the extension of the file (excluding '.')
     * @return true if the file should be packaged as standard java resources.
     */
    public static boolean checkFileForPackaging(String fileName, String extension) {
        // ignore hidden files and backup files
        if (fileName.charAt(0) == '.' || fileName.charAt(fileName.length()-1) == '~') {
            return false;
        }

        return !"aidl".equalsIgnoreCase(extension) &&           // Aidl files
                !"rs".equalsIgnoreCase(extension) &&            // RenderScript files
                !"fs".equalsIgnoreCase(extension) &&            // FilterScript files
                !"rsh".equalsIgnoreCase(extension) &&           // RenderScript header files
                !"d".equalsIgnoreCase(extension) &&             // Dependency files
                !"java".equalsIgnoreCase(extension) &&          // Java files
                !"scala".equalsIgnoreCase(extension) &&         // Scala files
                !"class".equalsIgnoreCase(extension) &&         // Java class files
                !"scc".equalsIgnoreCase(extension) &&           // VisualSourceSafe
                !"swp".equalsIgnoreCase(extension) &&           // vi swap file
                !"thumbs.db".equalsIgnoreCase(fileName) &&      // image index file
                !"picasa.ini".equalsIgnoreCase(fileName) &&     // image index file
                !"package.html".equalsIgnoreCase(fileName) &&   // Javadoc
                !"overview.html".equalsIgnoreCase(fileName);    // Javadoc
    }
}
